using System;
using System.Globalization;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using System.Security.Principal;
using System.Threading;
using System.Web;
using System.Web.Security;
using Sedna.Core.AppConfig;
using ICSharpCode.SharpZipLib.GZip;
using log4net;

namespace Sedna.Core.Security
{
	/// <summary>
	/// A static utility class that incapsulates the FormBased authentication specifics logic.
	/// This includes cookie preparation, redirection from a login page and injecting principal
	/// (at OnAuthenticate request from the Global.asax) and signing-out logic, which includes
	/// current thread principal and culture settings cleanup.
	/// 
	/// To store the <see cref="FormAuthCookieUserData"/> (which is basically a principal 
	/// with its roles and a desired culture info name) the class uses a GZIP algorythm 
	/// to reduce the size of the serialized data. Please make sure that your deployment has appropriate
	/// library in its path installed (ICSharpCode.SharpZipLib from http://www.icsharpcode.net/OpenSource/SharpZipLib/Default.aspx).
	/// </summary>
	public class PrincipalCookieUtil
	{
		private static ILog log = LogManager.GetLogger(typeof(PrincipalCookieUtil));

		private static int timeout = 30;//min


		static PrincipalCookieUtil()
		{
            string timeoutStr = AppConfigurationHolder.LoginTimeout().ToString();
			if(timeoutStr != null && string.Empty != timeoutStr)
			{
				try
				{
					timeout = int.Parse(timeoutStr);
					if(timeout <= 0)
					{
						timeout = 30;
						log.Error("'login.timeout' must be a positive integer. Specified value ignored, using the default one = 30 min.");
					}
				}
				catch(Exception e)
				{
					timeout = 30;
					log.Error("Unable to parse 'login.timeout' application settings. A value must be a valid positive integer in minutes, but was ='" + timeoutStr + "'. Using the default one = 30 min.", e);
				}
			}
			log.Info("Using login timeout = " + timeout);
		}


		/// <summary>
		/// serializes and gzippes the passed user data (principal and culture name)
		/// </summary>
		/// <param name="userData">principal and culture info</param>
		/// <returns>Base64 encoded string that containes serialized and gzipped principal with roles and culture info</returns>
		/*private static string SerializeCookieUserData(FormAuthCookieUserData userData)
		{
			MemoryStream stream = new MemoryStream();
			GZipOutputStream gstream = new GZipOutputStream(stream);
			BinaryFormatter formatter = new BinaryFormatter();
			formatter.Serialize(gstream, userData);
			gstream.Close();
			byte[] buf = stream.GetBuffer();
			return Convert.ToBase64String(buf);
		}*/

		/// <summary>
		/// deserializes and ungzippes supplied Base64 encoded user data.
		/// </summary>
		/// <param name="serialized">Base64 encoded, gzipped serialized user data</param>
		/// <returns>an instance of deserialized user data that contains a principal with its roles installed and a culture info name</returns>
		/*private static FormAuthCookieUserData DeserializeCookieUserData(string serialized)
		{
			byte[] buf = Convert.FromBase64String(serialized);
			BinaryFormatter formatter = new BinaryFormatter();
			return (FormAuthCookieUserData)formatter.Deserialize(new GZipInputStream(new MemoryStream(buf)));
		}*/


		/// <summary>
		/// prepares an HttpCookie that stores the authentication information (principal with roles and culture info)
		/// It uses the FormAuthenticationTicket to store this information.
		/// It renews the ticket for 30 minutes more (by default, however, this can be redefined by the 'login.timeout' application setting (in minutes))
		/// every call to this method (normally happens at Login).
		/// It encrypts the cookie value with the <c>FormsAuthentication.Encrypt()</c> method. 
		/// This method generates the encryption key on the fly during the application start up.
		/// Therefore, if you happen to plan to deploy the System on load-balancer, it is vital that both 
		/// instances of the Web application share the same encryption key. This can be archived 
		/// by setting the value explicitely in both Web.config files (on both hosts). Nothe that in this case
		/// the files become a potential vulnarability since it is possible to construct a cookie and encrypt
		/// it with the key stored in those files. Please take appropriate security measures to protect 
		/// those files from being read. How to install the key value in the file is described in the .Net 
		/// documentation, please refer to it for the detail instructions.
		/// 
		/// The cookie is leased for the predefined period (30 min by default, or equals to the amount set in 'login.timeout' app settings).
		/// After that period the cookie is considered to be gone off and a User is redirected to a login page.
		/// 
		/// </summary>
		/// <param name="principal">an instance of authenticated principal which is correctly filled with roles</param>
		/// <param name="cultureInfo">selected CultureInfo for a principal</param>
		/// <returns>prepared HpptCookie to be directly inserted into a response</returns>
		/*private static HttpCookie PrepareCookie(IPrincipal principal, CultureInfo cultureInfo)
		{
            SednaWorkPrincipal workPrincipal = principal as SednaWorkPrincipal;
			string serializedUserData = SerializeCookieUserData(new FormAuthCookieUserData(workPrincipal.getCookiePrincipal(), cultureInfo));
			FormsAuthenticationTicket ticket 
				= new FormsAuthenticationTicket(1, 
				principal.Identity.Name, 
				DateTime.Now, 
//				DateTime.Now + new TimeSpan(0, 5, 0), 
				DateTime.Now.AddMinutes(timeout),
				false, 
				serializedUserData
				);

			string cookieval = FormsAuthentication.Encrypt(ticket);

			HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName);
			cookie.Value = cookieval;
			cookie.Path = FormsAuthentication.FormsCookiePath;
			//cookie.Expires = DateTime.Now + new TimeSpan(0, 5, 0);
			cookie.Expires = DateTime.MinValue;

			return cookie;
		}*/


		/// <summary>
		/// a utility method that performs standard procedure of redirecting a request from a login page
		/// to a target requested resource
		/// </summary>
		public static void RedirectFromLoginPage()
		{
            try
            {
//			    HttpCookie cookie = PrincipalCookieUtil.PrepareCookie(Thread.CurrentPrincipal, Thread.CurrentThread.CurrentCulture);
//			    HttpContext.Current.Response.Cookies.Add(cookie);


                log.Debug(String.Format("Redirecting to: {0}", FormsAuthentication.GetRedirectUrl(Thread.CurrentPrincipal.Identity.Name, false)));
                HttpContext.Current.Response.Redirect(FormsAuthentication.GetRedirectUrl(Thread.CurrentPrincipal.Identity.Name, false));
			}
		    catch (Exception e)
		    {
		        log.Error("Login error: "+e.Message);
		        //Ignore
		    }
		}


		/// <summary>
		/// obtaines an instance of authenticated principal and its selected culture info name from a cookie,
		/// deserializes them and injects into a current thread.
		/// This method must be called at every request to restore the fact of authentication from the 
		/// Global.asax <c>Application_AuthenticateRequest()</c> method.
		/// In case something goes wrong (e.g. the method cannot restore authentication info from a cookie)
		/// or any other exception is thrown, the method silently logs them, ignores and singes a currently authenticated user out.
		/// After that a request is redirected to a default login page.
		/// (TODO: think about actual User sign out from the underlying authentication sub-system)
		/// </summary>
		/// <param name="httpContext">current HttpContext</param>
		public static void InjectPrincipal(HttpContext httpContext)
		{
			if(HttpContext.Current.Session == null)
			{
				return;
			}
			/*if(httpContext.User.Identity.IsAuthenticated)
			{
				try
				{*/
					UserMapper.RestoreCurrentState();
				    //HttpCookie cookie = httpContext.Request.Cookies[FormsAuthentication.FormsCookieName];
					//FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(cookie.Value);
					//string serializedUserData = ticket.UserData;
					//FormAuthCookieUserData userData = PrincipalCookieUtil.DeserializeCookieUserData(serializedUserData);
					//SednaCookiePrincipal cookiePrincipal = userData.Principal as SednaCookiePrincipal;
                    //httpContext.User = cookiePrincipal.getWorkPrincipal();
					//Thread.CurrentPrincipal = cookiePrincipal.getWorkPrincipal();
					SednaWorkPrincipal wp = Thread.CurrentPrincipal as SednaWorkPrincipal;
				    
				    Thread.CurrentThread.CurrentCulture = wp.Culture;
					Thread.CurrentThread.CurrentUICulture = wp.Culture;
					//HttpCookie renewedCookie = PrincipalCookieUtil.PrepareCookie(cookiePrincipal.getWorkPrincipal(), Thread.CurrentThread.CurrentCulture);
					//httpContext.Response.Cookies.Add(renewedCookie);
				/*}
				catch(Exception ex)
				{
					if(ex is OutOfMemoryException || ex is StackOverflowException || ex is ExecutionEngineException)
					{
						throw;
					}
					log.Error("unable restore a principal", ex);
					SignOut(httpContext);
				}
			}*/
			
		}

		/// <summary>
		/// singes out a current principal from the HttpContext.
		/// Note this method does not sign-out a principal from an underlying 
		/// authentication schema, thus appropriate call to it should be made.
		/// (TODO: subject to refactor - how should we automate this process in case of login or principal injection failure?)
		/// </summary>
		/// <param name="httpContext"></param>
		public static void SignOut(HttpContext httpContext)
		{
			IPrincipal anonymous = new WindowsPrincipal(WindowsIdentity.GetAnonymous());
			Thread.CurrentPrincipal = anonymous;
			Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
			Thread.CurrentThread.CurrentUICulture = CultureInfo.InvariantCulture;
			httpContext.User = anonymous;
			try{httpContext.Session.Abandon();}
			catch(Exception){}

			FormsAuthentication.SignOut();
		}


	}
}
